查看原文
其他

应用 | Build with Mapbox —— 基于Mapbox GL JS 的 3D飞机大战游戏

Alex Mapbox 2019-06-02


最早接触 Mapbox 还是在读研的时候,当时觉得底图好漂亮,能把地图设计做到如此精益求精,算是数一数二的企业。后来接触 Mapbox GL JS, 体验到了畅快的三维地图开发,想想自己这么爱玩游戏,干嘛不开发一款 3D 多人飞机大战呢?


受群里研究 Mapbox 的热情和@扯淡大叔, @老羽,以及@F3earth 成员的帮助和启发, 突发奇想花了点时间基于 Mapbox GL JS 和 Socket.io 做了一个 3D 飞机大战游戏。



整体架构


简单来说,整个游戏的设计思路就是:


  • 服务器:从启动开始就监听任何客户端发来了websocket 连接请求,有了连接(connection事件)后,就把客户端初次发来的用户名称、当前飞机的坐标、朝向作为一个client 加入客户端数据池(目前简单处理为clients数组)中。那么至此一个客户端的数据就同步到了服务器端了。

  • 客户端:将地图中的飞机坐标、朝向等信息也定时发送(socket.send)给服务器端,以便于服务器端同时广播(broadcast)给所有其他客户端。其实服务端就是负责中转消息,目的是让所有客户端视野中的玩家的飞机状态保持一致。既然websocket是双向通信,客户端也需要定时发送消息给服务器端,并且更重要的是处理服务器端发来的各种消息(message事件),进而推动游戏的进度,以及分辨哪些是欢迎用户上线的消息,普通的玩家位置同步消息,抑或是 A 击败了 B 这样的消息。

  • Mapbox GL JS:借助 Mapbox GL JS 展现宏观的战场和各飞机的状态,以及用户可以通过键盘对自己的战机进行细微的操作。Mapbox GL JS 的 Webgl 渲染及完备的  API 给 3D游戏开发带来的很大的便利。^_^


后端及前端websocket


简单看看后端 Websocket 代码是如何实现的:

// 后端关键流程实现
var app = express(),    server = require('http').createServer(app),    io = require('socket.io').listen(server); // 引入socket.io 库
io.on('connection', function(socket) {
   // 开始监听客户端的websocket连接请求,connection事件产生 socket 对象    socket.emit('open'); // 向该客户端发送open事件.    // init client drone obj for each connection !!    var client = {        name: false,        color: getColor(),        direction: 0,        coordinates: [0, 0]    }    // message from client.    socket.on('message', function(msg) {        
       if (!client.name && msg.name) {
           // 如果是第一次连接,把用户的名字存储起来,并且广播给所有客户端。            
           var obj  = { }; // 构建发送给其他客户端的消息            obj = msg;          
           clients.push(client); // 加入后台维持的客户端数组            
           socket.broadcast.emit('message', obj); // 广播欢迎语给其他客户端        } else if ( client.name == msg.name ) { // 客户端发来的飞机状态消息            // 广播给其他客户端            
           socket.broadcast.emit('message', obj);        }    } }


后台处理过程相对简单,基本只需接受某客户端发来的消息,转发给其他客户端即可(随机敌机位置什么的就不讲了,当然后期要改成所有客户端共享一套敌机信息,这样就可以一起打同一个BOSS了)。


前端业务相对复杂, 除了应对websocket 消息之外,需要构建一套飞机的数据模型,包括位置,速度,朝向,血量,武器装备等(可以非常复杂,目前就简单处理)。

var socket;
try {    socket = io.connect("http://123.206.201.245:3002");    
   socket.on('open', function(){  // 当服务端确认连接后,开始发送第一次数据。        statusBar.innerText = "已经连上服务器..";        
       var askName = prompt("来,取个名字", "");    }    
   socket.on("message", function(json) {
       // 其实收到的是js 对象,这一点很牛逼。因为双向通信过程中传递的是 Binary 形式的数据,不需要再次解析了。        if (json.type === "welcome" && json.text.name) {          
       // .. 显示其他用户登录消息        }
       else if (json.type === "defeat") {          
       // .. 在前端的敌机数据模型中移除空血槽的飞机        }
       else if (drone && json.text.name != drone.name) {          
           // .. 传来的其他客户端飞机消息            
           featureCol.features.forEach(function(drone) {          
           // featureCol 是所有敌机数据集合,根据用户名check是更新还是新增.            }        }    }
}


Mapbox GL JS 处理地图中的飞机、子弹及敌机


  • 飞机的状态数据需要定时上传服务器,同时借助 VectorLayer 渲染在 Map 中。渲染过程采用 GeoJSON 对象 作为飞机矢量图层 的数据源(source)。那么是否是一接到服务器端消息就去重绘所有飞机位置呢? 并不是这样,为了性能不那么差,这边通过全局的一个定时器去统一调用source 的 setData() 方法,实现飞机最新的状态重绘。

map.getSource('drone').setData(featureCol);
  • 飞机子弹的轨迹计算,涉及到用户按下空格键的瞬间飞机的位置和朝向,根据设定的子弹飞行时间做一个动画显示。

var bulletSource;
var bulletTimer = setInterval(renderBullet, 30);
// common function for render myDrone and other client's fire
function renderBullet() {    
   var steplength = 0.02;    
   particles.coordinates = [];    
   // if drone is firing, it's bullet coordiantes be calculated and rendered.    for (var j = 0; j < drones.length; j++) {        
       var hitted = false;        
       if (drones[j].firing && drones[j].bullet) {            drones[j].bullet.spoint.coordinates[0] += Math.sin(drones[j].bullet.direction)*steplength;            drones[j].bullet.spoint.coordinates[1] += Math.cos(drones[j].bullet.direction)*steplength;            
           var real_point = drones[j].bullet.spoint;            
           particles.coordinates.push(real_point.coordinates);            
           for (var i = 0; i < 9; i++) {                
               var particle = [];                
               // 0.01 is step length of bullet each frames..                particle.push(real_point.coordinates[0] - Math.sin(drones[j].bullet.direction)*steplength*i/zoom);                
               particle.push(real_point.coordinates[1] - Math.cos(drones[j].bullet.direction)*steplength*i/zoom);                
               particles.coordinates.push(particle);            }        }    }    bulletSource = map.getSource('drone-target');    
   if (bulletSource) {        
       bulletSource.setData(particles);    } }
  • 子弹和敌机的碰撞检测,简化处理:设定一个常数作为飞机体积,在子弹飞行过程中实时计算子弹和敌机实际地理距离,小于飞机体积,则判定为碰撞。

function testCrash(coordinates, name) {    
   var distance, volume = 0.20/zoom,
       featureIndex = 0,
       damagedIndex = -1,
       damagedDroneName = 0,
       hitted = false;        ......    // 只用来做本玩家对其他飞机的射击检测!!!    if (damagedDroneName && damagedIndex > -1 ){        
       var damagedFeature = findInFeatures(damagedDroneName);        
       var damagedDrone = findInDrones(damagedDroneName);        
       if (!damagedDrone) return;        
       if (damagedDrone.life)
           damagedDrone.life -= 1;        
       if (name === drone.name && !damagedDrone.life) {            totalKill += 1;            
           statsBar.innerText = "Kill " + totalKill;        }        
       if (!damagedDrone.life) {            
           // explode effect on damagedFeature.            explode(damagedFeature, firingTime - 200);            
           if (damagedDroneName !== drone.name) {                
           setTimeout(function(){                    
               delInDrones(damagedDroneName);                 }, 50);                        setTimeout(function(){                    
               delInFeatureCol(damagedDroneName);                 }, firingTime - 100);            }        }        hitted = true;    }    
   return hitted; }
  • 敌机自动跟随最近玩家的设定。这个奇怪的设定是后来加上去的,因为最开始的敌机我设定为攻击最近的飞机,不论是Robot还是玩家,并且敌机移动是随机方向的。后来为了玩家有种被追杀、热血沸腾的感觉,设定为了追击玩家。。


另外挑两点比较有意思的Code分享下,第一点是 Robot 敌机的随机行为控制:

// setPostion is to update Mydrone position.
function setPosition() {    
   // direction in Rad. Generally, 1 Rad stands for 100km    var current_rotate = map.getBearing();    if (!manual && Math.random() > 0.95) {
       // 这边有意思,在每秒50帧的情况下,不是每一帧都会随机微调飞机的方向。而是5%的概率。        direction += (Math.random() - 0.5) /5;    }        // 根据飞机朝向和速度更新位置。    point.coordinates[0] += speed * Math.sin(direction) / 100;    
   point.coordinates[1] += speed * Math.cos(direction) / 100;    
   // 校正飞机的朝向显示。因为默认情况下mapbox是根据你的视角随时调整图标方向。但实际上飞机图标的朝向必须和飞机运行方向一致,而不是简单的和标注一样。    current_rotate = (-current_rotate) + direction * (180 / Math.PI); }


第二点是子弹飞行的计算过程:

// start: fire location, target: bullet destination, duration: total animation time
function renderBulvar(start, target, direction, duration) {    
   // target is geojson POINT, add Temp point in layer..    var interval = 20,
       ratio = interval/duration,
       real_point = start,
       range = 0.4,
       count = 0,
       hitted = false;    
   if (target.coordinates) {        
       var targetSource = map.getSource('drone-target');        
       window.setInterval(function(){            
           if (count > duration/interval) {
               // 到达终点,不计算了            } else {                
               // 子弹每一帧跑一定比例的路程,最终到达指定终点                real_point.coordinates[0] += Math.sin(direction)*ratio*range;
               real_point.coordinates[1] += Math.cos(direction)*ratio*range;                
               targetSource.setData(real_point);                
               if (!hitted){                    hitted = testCrash(real_point.coordinates); // 感觉这里的hitted 有问题.                }                count += 1;            }        }, interval);    } }


写在最后


为了游戏完整性,陆续加上了聊天系统、 飞机生命值、小地图以及敌机状态面板(鼠标悬停任意战机,查看当前状态)。


到这里其实基本介绍了这个游戏的制作过程,经历了一些不成熟的想法,总共花了十几个小时完成目前的开发。还没有严谨地考虑过代码结构和重构,有好些 Bug 都在慢慢去修改。感兴趣的童鞋,想在线体验游戏或者有任何建议的,欢迎访问github项目地址: Jqmap2 。


本文由 Alex 同学撰写,非常感谢Alex 同学的分享。还不赶快参加 Mapbox 组织的 "Build with Mapbox" 活动,直接联系我们分享使用 Mapbox 产品的开发经验,赢取礼物🎁和奖金!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存